use num_bigint::BigUint;
use tokio::sync::mpsc;
use tracing::{debug, error, info, trace, warn};

use bc_base::{
    BlockHeight, BlockId, PublicKeyHashBytes, TransactionId, TransactionValue,
    block::Block,
    constants::{HASH160_BYTES_LENGTH, MAX_BLOCK_SIZE},
    transaction::Transaction,
};
use bc_lib::{mining::MiningCommand, transaction};
use bc_manager::BcManagerHandler;

pub async fn mining_task(
    mut command_receiver: mpsc::Receiver<MiningCommand>,
    mut transaction_receiver: mpsc::Receiver<Transaction>,
    mut shutdown_signal: tokio::sync::watch::Receiver<bool>,
    bc_manager: BcManagerHandler,
) {
    let mut mining_active = false;
    let mut miner_id = PublicKeyHashBytes::new([0; HASH160_BYTES_LENGTH]);
    let mut extra_nonce = BigUint::from(0u32);

    let mut block_for_mining: Option<PreparatoryBlock> = None;
    let mut gen_block_when_receive_tx = false;

    loop {
        tokio::select! {
            _ = shutdown_signal.changed() => {
                if *shutdown_signal.borrow() {
                    info!("Shutdown signal received for mining task.");
                    break;
                }
            },

            Some(command) = command_receiver.recv() => {
                info!("Mining command received: {:?}", command);
                // println!("Mining command received: {:?}", command);
                match command {
                    MiningCommand::Start(pub_key_hash) => {
                        miner_id = pub_key_hash;
                        block_for_mining = Some(create_preparatory_block(&bc_manager, &extra_nonce, &miner_id).await);

                        mining_active = true;
                        info!("Mining started.");
                    },
                    MiningCommand::Pause => {
                        mining_active = false;
                        info!("Mining paused.");
                    },
                    MiningCommand::Stop => {
                        info!("Mining stopped.");
                        break;
                    },
                    MiningCommand::DeleteTx(tx_id) => {
                        info!("Deleting transaction: {}", tx_id);
                        match block_for_mining.as_mut() {
                            Some(blk_for_mining) => {
                                blk_for_mining.delete_transaction(&tx_id);
                            },
                            None => {
                                error!("Block for mining is None!");
                            },
                        }
                    },
                    MiningCommand::GenBlockWhenReceiveTx(t) => {
                        info!("Received transaction, generating block when receive tx: {}", t);
                        gen_block_when_receive_tx = t;
                        if t {
                            bc_manager
                            .lock()
                            .await
                            .set_gen_block_when_receive_tx_flag(true);
                        }
                    },
                    MiningCommand::StartMiningImmediately => {
                        if block_for_mining.is_none() {
                            if miner_id.is_null() {
                                warn!("Miner ID is null, cannot start mining immediately.");
                                continue;
                            }
                            block_for_mining = Some(create_preparatory_block(&bc_manager, &extra_nonce, &miner_id).await);
                        }

                        if let Some(blk_for_mining) = block_for_mining.take() {
                            do_mining_and_process_block(blk_for_mining, bc_manager.clone(), &mut extra_nonce).await;
                        } else {
                            warn!("Block for mining is None when trying to mine immediately.");
                        }
                    }
                }
            },

            Some(tx) = transaction_receiver.recv() => {
                if !mining_active {
                    warn!("Mining is paused, ignoring transaction:\n{:?}", tx);
                    continue;
                }

                info!("Received transaction, starting mining (miner-id: {})...", miner_id);
                // println!("Received transaction, starting mining (miner-id: {})...", miner_id);
                if block_for_mining.is_none() {
                    if miner_id.is_null() {
                        warn!("Miner ID is null, cannot start mining.");
                        continue;
                    }
                    block_for_mining = Some(create_preparatory_block(&bc_manager, &extra_nonce, &miner_id).await);
                }

                let mut blk_for_mining = block_for_mining.unwrap();
                let tx_len = tx.serialize().len(); // todo
                let tmp_len = blk_for_mining.get_serialize_len() + tx_len;

                if gen_block_when_receive_tx {
                    debug!("Generate block immediately when receive tx.");
                    // println!("Generate block immediately when receive tx.");

                    blk_for_mining.add_transaction(tx, tx_len);
                    do_mining_and_process_block(blk_for_mining, bc_manager.clone(), &mut extra_nonce).await;
                    block_for_mining = Some(create_preparatory_block(&bc_manager, &extra_nonce, &miner_id).await);

                    bc_manager
                    .lock()
                    .await
                    .set_gen_block_when_receive_tx_flag(false);
                    trace!("Mining and processing block done.");

                    continue;
                }

                match tmp_len.cmp(&MAX_BLOCK_SIZE) {
                    std::cmp::Ordering::Less => {
                        debug!("Block is not full, add transaction and waiting for more transactions.");
                        blk_for_mining.add_transaction(tx, tx_len);
                        block_for_mining = Some(blk_for_mining);
                    },

                    std::cmp::Ordering::Equal => {
                        debug!("After add tx, Block is full, mining and processing block.");
                        blk_for_mining.add_transaction(tx, tx_len);
                        do_mining_and_process_block(blk_for_mining, bc_manager.clone(), &mut extra_nonce).await;
                        block_for_mining = Some(create_preparatory_block(&bc_manager, &extra_nonce, &miner_id).await);
                    },

                    std::cmp::Ordering::Greater => {
                        debug!("Block is full, mining and processing block.");
                        do_mining_and_process_block(blk_for_mining, bc_manager.clone(), &mut extra_nonce).await;

                        let mut t = create_preparatory_block(&bc_manager, &extra_nonce, &miner_id).await;
                        t.add_transaction(tx, tx_len);
                        block_for_mining = Some(t);
                    },
                }
            },
        }
    }
}

async fn create_preparatory_block(
    bc_manager: &BcManagerHandler,
    extra_nonce: &BigUint,
    miner_id: &PublicKeyHashBytes,
) -> PreparatoryBlock {
    trace!(
        "Create preparatory block, miner_id: {}, extra_nonce: {:#x}",
        miner_id, extra_nonce
    );

    let bc_manager = bc_manager.lock().await;
    let nbits = bc_manager.get_next_work_required_for_mining().await;
    let block_height = bc_manager.read_best_height().await;
    let subsidy = bc_manager.get_subsidy_for_mining().await;
    let block_id = bc_manager.read_latest_block_id_in_best_chain().await;
    let t = PreparatoryBlock::new(
        nbits,
        block_height,
        extra_nonce,
        miner_id,
        subsidy,
        &block_id,
    );

    trace!("Create preparatory block done.");

    t
}

async fn do_mining_and_process_block(
    blk_for_mining: PreparatoryBlock,
    bc_manager: BcManagerHandler,
    extra_nonce: &mut BigUint,
) {
    debug!("Do mining and process block, extra_nonce: {}", extra_nonce);

    let Some(blk) = do_mining(&bc_manager, blk_for_mining).await else {
        warn!("Mining failed.");
        return;
    };

    if let Err(e) = bc_manager
        .lock()
        .await
        .broadcast_block_to_p2p_network(blk.clone())
        .await
    {
        error!("Error broadcasting block: {:?}", e);
    }
    if let Err(e) = bc_manager.lock().await.process_block(&blk).await {
        error!("Error processing block: {:?}", e);
        // println!("Error processing block: {:?}", e);
    } else {
        debug!("Mining a block({}).", blk.calc_block_id());
        println!("Mining a block({}).", blk.calc_block_id());
    }

    *extra_nonce += BigUint::from(1u32);
}

async fn do_mining(
    bc_manager: &BcManagerHandler,
    mut block_for_mining: PreparatoryBlock,
) -> Option<Block> {
    // let bc_manager = bc_manager.lock().await;
    // let nbits = bc_manager.get_next_work_required_for_mining().await;
    // block_for_mining.set_nbits(nbits);

    if let Ok(merkle_root_hash) = block_for_mining.block.build_merkle_tree() {
        block_for_mining.block.set_merkle_root(merkle_root_hash);
    } else {
        error!("Error calculating merkle root hash.");
        return None;
    }

    let block_time = bc_manager.lock().await.get_adjusted_timestamp().await;
    debug!("Mining block, block_time: {}", block_time);
    block_for_mining.block.set_timestamp(block_time as u32);
    block_for_mining.block.find_and_set_valid_nonce();

    Some(block_for_mining.block)
}

struct PreparatoryBlock {
    block: Block,
    serialize_len: usize,
}

impl PreparatoryBlock {
    pub fn new(
        nbits: u32,
        block_height: BlockHeight,
        extra_nonce: &BigUint,
        miner_pubkey_hash: &PublicKeyHashBytes,
        subsidy: TransactionValue,
        prev_block_header_hash: &BlockId,
    ) -> Self {
        let coin_base = transaction::create_coinbase_for_mining(
            nbits,
            block_height,
            extra_nonce,
            miner_pubkey_hash,
            subsidy,
        );
        let mut block = Block::new_with_nbits(nbits, prev_block_header_hash);
        block.add_transaction(coin_base);
        let serialize_len = block.serialize().len();
        Self {
            block,
            serialize_len,
        }
    }

    pub fn add_transaction(&mut self, tx: Transaction, tx_serialize_len: usize) {
        self.block.add_transaction(tx);
        self.serialize_len += tx_serialize_len;
    }

    pub fn get_serialize_len(&self) -> usize {
        self.serialize_len
    }

    pub fn delete_transaction(&mut self, tx_id: &TransactionId) {
        let len = self.block.delete_transaction(tx_id);
        self.serialize_len -= len;
    }
}
